import { Sky } from "three/examples/jsm/objects/Sky";
import { App } from "../application";
import { Color, Fog, MathUtils, Mesh, PMREMGenerator, PlaneGeometry, SphereGeometry, ShaderMaterial, Texture, TextureLoader, Vector3, BackSide, CylinderGeometry, MeshBasicMaterial, NoBlending } from "three";
import Helper from "../utils/Helper";
import Events from "../application/constants/Events";
import Enum from "../application/constants/Enum";
import Const from "../application/constants/Const";

class BackGround {
    private app: App;
    public sky: Sky;
    private cyliderMesh: Mesh;
    public options;
    public backgroudMesh: Mesh;
    public fog: Fog;
    public fogDistance: number;
    private cylinderOffseZ: number;
    private skyOffsetZ: number;
    public type: string = 'Fog';
    private changeFogEvent;

    constructor() {
        this.app = App.getInstance();
        this.options = this.app.options.BackGround;
        this.skyOffsetZ = 6000000;
        this.cylinderOffseZ = 16000000;
        this.fogDistance = Infinity;
        this.set(this.options);

        this.changeFogEvent = this.changeFog.bind(this); //Helper.debounce2(this.changeFog.bind(this), 100, false); //
        this.app.on(Events.CameraChange, this.changeFogEvent);
    }

    /**
     * 设置背景
     * @param options 背景配置
     * @example  
     * //无背景：
       window.app.options.BackGround.background = "None";
       window.app.background.set(window.app.options.BackGround);
       //雾:
       window.app.options.BackGround.background = "Fog";
       window.app.background.set(window.app.options.BackGround);
       //背景颜色：
       window.app.options.BackGround.background = "Color";
       window.app.options.BackGround.backGroundColor = prompt('背景颜色：', window.app.options.BackGround.backGroundColor) || '#00BFFF';
       window.app.background.set(window.app.options.BackGround);
       //背景渐变颜色
       window.app.options.BackGround.background = "Gradient";
       window.app.options.BackGround.backGroundColorA = prompt('背景下端颜色：', window.app.options.BackGround.backGroundColorA) || '#0000FF';
       window.app.options.BackGround.backGroundColorB = prompt('背景上端颜色：', window.app.options.BackGround.backGroundColorB) || '#FF0000';
       window.app.background.set(window.app.options.BackGround);
       //背景图片
       window.app.options.BackGround.background = "Image";
       window.app.options.BackGround.imageUrl = prompt('背景图片：', window.app.options.BackGround.imageUrl);
       window.app.background.set(window.app.options.BackGround);
       //天空球
       window.app.options.BackGround.background = "Sky";
       window.app.background.set(window.app.options.BackGround);
     */
    public set(options) {
        this.clearBackGround();
        let type = options.background;
        this.type = type;
        switch (type) {
            case Enum.BackGround.None: {
                break;
            }
            case Enum.BackGround.Color: {
                this.createBackGroundMesh();
                (<ShaderMaterial>this.backgroudMesh.material).uniforms.useImage.value = false;
                (<ShaderMaterial>this.backgroudMesh.material).uniforms.uColorA.value = new Color(options.backGroundColor);
                (<ShaderMaterial>this.backgroudMesh.material).uniforms.uColorB.value = new Color(options.backGroundColor);
                break;
            }
            case Enum.BackGround.Gradient: {
                this.createBackGroundMesh();
                (<ShaderMaterial>this.backgroudMesh.material).uniforms.useImage.value = false;
                (<ShaderMaterial>this.backgroudMesh.material).uniforms.uColorA.value = new Color(options.backGroundColorA);
                (<ShaderMaterial>this.backgroudMesh.material).uniforms.uColorB.value = new Color(options.backGroundColorB);
                break;
            }
            case Enum.BackGround.Image: {
                this.createBackGroundMesh();
                const loader = new TextureLoader(undefined);
                let scope = this;
                let texture = loader.load(options.imageUrl, function () {
                    (<ShaderMaterial>scope.backgroudMesh.material).uniforms.useImage.value = true;
                    (<ShaderMaterial>scope.backgroudMesh.material).uniforms.uTexture.value = texture;
                });

                break;
            }
            case Enum.BackGround.Fog: {
                this.createFog();
                this.app.on(Events.MapLoadFinish, this.changeFog.bind(this));
                // this.changeFog();
                break
            }
            case Enum.BackGround.Sky: {
                this.createSky();
                break
            }
            default: {
                this.clearBackGround();
            }
        }

        let camera = this.app.three.camera;
        let projectionMatrix = camera.projectionMatrix.clone();
        let matrixWorld = camera.matrixWorld.clone();
        this.app.fireEvent(Events.CameraChange, projectionMatrix, matrixWorld); //强制刷新
    }


    private createSky() {
        this.sky = new Sky();
        this.sky.scale.setScalar(45000000);
        // @ts-ignore
        this.sky.geometry = new SphereGeometry(1, 64, 64, 0, Math.PI * 2, 0, Math.PI * 0.5);
        this.sky.material.depthTest = false;
        this.sky.renderOrder = Const.RenderOrder.Sky;
        this.sky.rotateX(Math.PI / 2);
        this.app.three.scene.add(this.sky);
        let position = this.app.three.camera.position.clone();
        this.sky.position.set(position.x, position.y, position.z - this.skyOffsetZ);

        let sun = new Vector3(0, 0, 0);
        let uniforms = this.sky.material.uniforms;
        uniforms.up.value.set(0, 0, 1);
        this.app.three.renderer.toneMappingExposure = 0.5;

        let scope = this;
        Helper.addPropertyListener(this.sky, ['turbidity', 'rayleigh', 'mieCoefficient', 'mieDirectionalG'], {
            getter: null,
            setter: (key, value) => {
                uniforms[key].value = value;
                scope.app.three.renderer.render(scope.app.three.scene, scope.app.three.camera);
            }
        });

        let pmremGenerator = new PMREMGenerator(scope.app.three.renderer);
        //@ts-ignore
        let renderTarget = pmremGenerator.fromScene(scope.sky);
        Helper.addPropertyListener(this.sky, ['elevation', 'azimuth'], {
            getter: null,
            setter: (key, value) => {
                const phi = MathUtils.degToRad(0 - scope.sky['elevation']);
                const theta = MathUtils.degToRad(scope.sky['azimuth']);

                sun.setFromSphericalCoords(1, phi, theta);

                uniforms['sunPosition'].value.copy(sun);
                scope.app.three.renderer.render(scope.app.three.scene, scope.app.three.camera);


                if (scope.app.options.Render.useSkyEnv) {
                    if (renderTarget !== undefined) renderTarget.dispose();
                    //@ts-ignore
                    renderTarget = pmremGenerator.fromScene(scope.sky, 0, 0.1, 10000000000);
                    scope.app.three.scene.environment = renderTarget.texture;
                }
            }
        });

        this.sky['turbidity'] = 10;
        this.sky['rayleigh'] = 3;
        this.sky['mieCoefficient'] = 0.005;
        this.sky['mieDirectionalG'] = 0.7;
        this.sky['elevation'] = 2;
        this.sky['azimuth'] = -180;
    }

    private createFog() {
        // this.app.options.Map.useCarpet = true; //开启地毯底图

        this.fog = new Fog(this.options.fogColor, Infinity, Infinity); //F5F5F5
        this.app.three.scene.fog = this.fog;

        //cyliderMesh是为了避免没有模型渲染的地方一片空白，导致雾化失效。cyliderMesh后期可以使用广告牌方案替换掉，但是这个广告牌的距离需要根据雾的距离动态调整
        let position = this.app.three.camera.position.clone();
        let cylinderGeometry = new CylinderGeometry(1, 1, 1, 64, 1, false);
        let cyliderMaterial = new MeshBasicMaterial({ color: this.options.fogColor, fog: true, side: BackSide, depthTest: false});
        this.cyliderMesh = new Mesh(cylinderGeometry, cyliderMaterial);
        this.cyliderMesh['canNotSelect'] = true; //不参与射线拾取
        this.cyliderMesh.renderOrder = Const.RenderOrder.BackGroundCylinderMesh;
        this.cyliderMesh.rotateX(Math.PI / 2);
        this.cyliderMesh.scale.setScalar(45000000);
        this.cyliderMesh.position.set(position.x, position.y, position.z - this.cylinderOffseZ);
        this.cyliderMesh.renderOrder = -Infinity;
        this.app.three.scene.add(this.cyliderMesh);
    }

    /**
     * 清空所有背景（恢复白色背景）
     */
    private clearBackGround() {
        if (this.backgroudMesh) {
            this.app.three.scene.remove(this.backgroudMesh);
            this.backgroudMesh = null;
        }

        if (this.sky) {
            this.app.three.scene.remove(this.sky);
            this.sky = null;
        }

        if (this.fog) {
            this.clearFog();
        }
    }

    private clearFog() {
        // if(this.app.three.scene.fog){
            this.fog = null;
            this.app.three.scene.fog = null;
            this.app.three.scene.remove(this.cyliderMesh);
            this.app.off(Events.CameraChange, this.changeFogEvent);
        // }
    }

    private createBackGroundMesh() {
        if (!this.backgroudMesh) {
            let shaderMaterial = new ShaderMaterial({
                uniforms: {
                    useImage: { value: false },
                    uColorA: { value: new Color() },
                    uColorB: { value: new Color() },
                    uTexture: { value: new Texture() }
                },
                vertexShader: 'varying vec2 vUv; void main(){vUv = uv;float depth = 1.; gl_Position = vec4(position.xy, depth, 1.0); }',
                fragmentShader: 'varying vec2 vUv;uniform vec3 uColorA;uniform vec3 uColorB;uniform sampler2D uTexture;uniform bool useImage;void main(){if (useImage) {  vec4 mapColor = texture2D(uTexture, vUv); gl_FragColor = mapColor;}else{ gl_FragColor = vec4( mix( uColorA, uColorB, vec3(vUv.y)), 1. );} }'
            });

            var myGradient = new Mesh(
                new PlaneGeometry(2, 2, 1, 1),
                shaderMaterial
            );
            this.backgroudMesh = myGradient;
            myGradient.material.depthTest = false;
            myGradient.renderOrder = Const.RenderOrder.BackGroundGradient;
            this.app.three.scene.add(this.backgroudMesh);

            let scope = this;
            this.app.on(Events.CameraChange, function () {
                if (scope.backgroudMesh) {
                    let pos = scope.app.camera.lookAtTarget;
                    scope.backgroudMesh.position.set(pos.x, pos.y, pos.z);
                }
            });
        }
    }

    private changeFog() {
        let position = this.app.three.camera.position.clone();
        if (this.fog) {
            let pitchAngle = this.app.map.getPitchAngle();
            let verticalVector = new Vector3(0, 0, -this.app.three.camera.position.z);
            let verticalLength = verticalVector.length();
            let lookAtTarget = this.app.camera.lookAtTarget.clone();
            let cameraPosition = this.app.three.camera.position;
            let slopeVector = new Vector3(lookAtTarget.x - cameraPosition.x, lookAtTarget.y - cameraPosition.y, lookAtTarget.z - cameraPosition.z);
            let cosValue = verticalVector.clone().normalize().dot(slopeVector.clone().normalize());
            let slopeDistance = verticalLength / cosValue;

            let startAngle = this.options.fogStartAngle || 20;
            let endAngle = this.app.options.Map.maxSlopeAngle;
            let maxDistanceFactor = 1.3;
            let minDistacceFactor = 1.0;
            let farDistanceScale = 1.6;
            if (pitchAngle >= startAngle && pitchAngle <= endAngle) {
                let scale = (maxDistanceFactor - ((maxDistanceFactor - minDistacceFactor) / (endAngle - startAngle)) * (pitchAngle - startAngle));
                
                slopeDistance = parseFloat(slopeDistance.toFixed(2));
                scale = parseFloat(scale.toFixed(2));

                this.fog.near = slopeDistance * scale;
                this.fog.far = slopeDistance * farDistanceScale;
            }
            else {
                this.fog.near = Infinity;
                this.fog.far = Infinity;
            }

            this.cyliderMesh.position.set(position.x, position.y, position.z + this.cylinderOffseZ);
        }
        if (this.sky) {
            this.sky.position.set(position.x, position.y, position.z - this.skyOffsetZ);
            if (this.cyliderMesh){
                this.cyliderMesh.position.set(position.x, position.y, position.z + this.cylinderOffseZ);
            }
        }
    }

}
export { BackGround }